Passed
Branch wavefile-rw (3e4b6e)
by Rafael S.
02:30
created

WaveFileParser.fromBuffer   A

Complexity

Conditions 2

Size

Total Lines 20
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 18
dl 0
loc 20
rs 9.5
c 0
b 0
f 0
cc 2

1 Function

Rating   Name   Duplication   Size   Complexity  
A WaveFileParser.getSmplLoopsBytes_ 0 14 2
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFileParser class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
import WaveFileReader from './wavefile-reader';
31
import writeString from './write-string';
32
import validateNumChannels from './validate-num-channels'; 
33
import validateSampleRate from './validate-sample-rate';
34
import {unpack, packTo, packStringTo, packString, pack} from 'byte-data';
35
36
/**
37
 * A class to read and write wav files.
38
 */
39
export default class WaveFileParser extends WaveFileReader {
40
41
  /**
42
   * Return a byte buffer representig the WaveFileParser object as a .wav file.
43
   * The return value of this method can be written straight to disk.
44
   * @return {!Uint8Array} A wav file.
45
   * @throws {Error} If bit depth is invalid.
46
   * @throws {Error} If the number of channels is invalid.
47
   * @throws {Error} If the sample rate is invalid.
48
   */
49
  toBuffer() {
50
    this.validateWavHeader();
51
    return this.writeWavBuffer_();
52
  }
53
54
  /**
55
   * Return the sample at a given index.
56
   * @param {number} index The sample index.
57
   * @return {number} The sample.
58
   * @throws {Error} If the sample index is off range.
59
   */
60
  getSample(index) {
61
    index = index * (this.dataType.bits / 8);
62
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
63
      throw new Error('Range error');
64
    }
65
    return unpack(
66
      this.data.samples.slice(index, index + this.dataType.bits / 8),
67
      this.dataType);
68
  }
69
70
  /**
71
   * Set the sample at a given index.
72
   * @param {number} index The sample index.
73
   * @param {number} sample The sample.
74
   * @throws {Error} If the sample index is off range.
75
   */
76
  setSample(index, sample) {
77
    index = index * (this.dataType.bits / 8);
78
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
79
      throw new Error('Range error');
80
    }
81
    packTo(sample, this.dataType, this.data.samples, index);
82
  }
83
84
  /**
85
   * Validate the header of the file.
86
   * @throws {Error} If bit depth is invalid.
87
   * @throws {Error} If the number of channels is invalid.
88
   * @throws {Error} If the sample rate is invalid.
89
   * @protected
90
   */
91
  validateWavHeader() {
92
    this.validateBitDepth_();
93
    if (!validateNumChannels(this.fmt.numChannels, this.fmt.bitsPerSample)) {
94
      throw new Error('Invalid number of channels.');
95
    }
96
    if (!validateSampleRate(
97
        this.fmt.numChannels, this.fmt.bitsPerSample, this.fmt.sampleRate)) {
98
      throw new Error('Invalid sample rate.');
99
    }
100
  }
101
102
  /**
103
   * Return a .wav file byte buffer with the data from the WaveFileParser object.
104
   * The return value of this method can be written straight to disk.
105
   * @return {!Uint8Array} The wav file bytes.
106
   * @private
107
   */
108
  writeWavBuffer_() {
109
    this.uInt16_.be = this.container === 'RIFX';
110
    this.uInt32_.be = this.uInt16_.be;
111
    /** @type {!Array<!Array<number>>} */
112
    let fileBody = [
113
      this.getJunkBytes_(),
114
      this.getDs64Bytes_(),
115
      this.getBextBytes_(),
116
      this.getFmtBytes_(),
117
      this.getFactBytes_(),
118
      packString(this.data.chunkId),
119
      pack(this.data.samples.length, this.uInt32_),
120
      this.data.samples,
121
      this.getCueBytes_(),
122
      this.getSmplBytes_(),
123
      this.getLISTBytes_()
124
    ];
125
    /** @type {number} */
126
    let fileBodyLength = 0;
127
    for (let i=0; i<fileBody.length; i++) {
128
      fileBodyLength += fileBody[i].length;
129
    }
130
    /** @type {!Uint8Array} */
131
    let file = new Uint8Array(fileBodyLength + 12);
132
    /** @type {number} */
133
    let index = 0;
134
    index = packStringTo(this.container, file, index);
135
    index = packTo(fileBodyLength + 4, this.uInt32_, file, index);
136
    index = packStringTo(this.format, file, index);
137
    for (let i=0; i<fileBody.length; i++) {
138
      file.set(fileBody[i], index);
139
      index += fileBody[i].length;
140
    }
141
    return file;
142
  }
143
144
  /**
145
   * Return the bytes of the 'bext' chunk.
146
   * @private
147
   */
148
  getBextBytes_() {
149
    /** @type {!Array<number>} */
150
    let bytes = [];
151
    this.enforceBext_();
152
    if (this.bext.chunkId) {
153
      this.bext.chunkSize = 602 + this.bext.codingHistory.length;
154
      bytes = bytes.concat(
155
        packString(this.bext.chunkId),
156
        pack(602 + this.bext.codingHistory.length, this.uInt32_),
157
        writeString(this.bext.description, 256),
158
        writeString(this.bext.originator, 32),
159
        writeString(this.bext.originatorReference, 32),
160
        writeString(this.bext.originationDate, 10),
161
        writeString(this.bext.originationTime, 8),
162
        pack(this.bext.timeReference[0], this.uInt32_),
163
        pack(this.bext.timeReference[1], this.uInt32_),
164
        pack(this.bext.version, this.uInt16_),
165
        writeString(this.bext.UMID, 64),
166
        pack(this.bext.loudnessValue, this.uInt16_),
167
        pack(this.bext.loudnessRange, this.uInt16_),
168
        pack(this.bext.maxTruePeakLevel, this.uInt16_),
169
        pack(this.bext.maxMomentaryLoudness, this.uInt16_),
170
        pack(this.bext.maxShortTermLoudness, this.uInt16_),
171
        writeString(this.bext.reserved, 180),
172
        writeString(
173
          this.bext.codingHistory, this.bext.codingHistory.length));
174
    }
175
    return bytes;
176
  }
177
178
  /**
179
   * Make sure a 'bext' chunk is created if BWF data was created in a file.
180
   * @private
181
   */
182
  enforceBext_() {
183
    for (let prop in this.bext) {
184
      if (this.bext.hasOwnProperty(prop)) {
185
        if (this.bext[prop] && prop != 'timeReference') {
186
          this.bext.chunkId = 'bext';
187
          break;
188
        }
189
      }
190
    }
191
    if (this.bext.timeReference[0] || this.bext.timeReference[1]) {
192
      this.bext.chunkId = 'bext';
193
    }
194
  }
195
196
  /**
197
   * Return the bytes of the 'ds64' chunk.
198
   * @return {!Array<number>} The 'ds64' chunk bytes.
199
   * @private
200
   */
201
  getDs64Bytes_() {
202
    /** @type {!Array<number>} */
203
    let bytes = [];
204
    if (this.ds64.chunkId) {
205
      bytes = bytes.concat(
206
        packString(this.ds64.chunkId),
207
        pack(this.ds64.chunkSize, this.uInt32_),
208
        pack(this.ds64.riffSizeHigh, this.uInt32_),
209
        pack(this.ds64.riffSizeLow, this.uInt32_),
210
        pack(this.ds64.dataSizeHigh, this.uInt32_),
211
        pack(this.ds64.dataSizeLow, this.uInt32_),
212
        pack(this.ds64.originationTime, this.uInt32_),
213
        pack(this.ds64.sampleCountHigh, this.uInt32_),
214
        pack(this.ds64.sampleCountLow, this.uInt32_));
215
    }
216
    //if (this.ds64.tableLength) {
217
    //  ds64Bytes = ds64Bytes.concat(
218
    //    pack(this.ds64.tableLength, this.uInt32_),
219
    //    this.ds64.table);
220
    //}
221
    return bytes;
222
  }
223
224
  /**
225
   * Return the bytes of the 'cue ' chunk.
226
   * @return {!Array<number>} The 'cue ' chunk bytes.
227
   * @private
228
   */
229
  getCueBytes_() {
230
    /** @type {!Array<number>} */
231
    let bytes = [];
232
    if (this.cue.chunkId) {
233
      /** @type {!Array<number>} */
234
      let cuePointsBytes = this.getCuePointsBytes_();
235
      bytes = bytes.concat(
236
        packString(this.cue.chunkId),
237
        pack(cuePointsBytes.length + 4, this.uInt32_),
238
        pack(this.cue.dwCuePoints, this.uInt32_),
239
        cuePointsBytes);
240
    }
241
    return bytes;
242
  }
243
244
  /**
245
   * Return the bytes of the 'cue ' points.
246
   * @return {!Array<number>} The 'cue ' points as an array of bytes.
247
   * @private
248
   */
249
  getCuePointsBytes_() {
250
    /** @type {!Array<number>} */
251
    let points = [];
252
    for (let i=0; i<this.cue.dwCuePoints; i++) {
253
      points = points.concat(
254
        pack(this.cue.points[i].dwName, this.uInt32_),
255
        pack(this.cue.points[i].dwPosition, this.uInt32_),
256
        packString(this.cue.points[i].fccChunk),
257
        pack(this.cue.points[i].dwChunkStart, this.uInt32_),
258
        pack(this.cue.points[i].dwBlockStart, this.uInt32_),
259
        pack(this.cue.points[i].dwSampleOffset, this.uInt32_));
260
    }
261
    return points;
262
  }
263
264
  /**
265
   * Return the bytes of the 'smpl' chunk.
266
   * @return {!Array<number>} The 'smpl' chunk bytes.
267
   * @private
268
   */
269
  getSmplBytes_() {
270
    /** @type {!Array<number>} */
271
    let bytes = [];
272
    if (this.smpl.chunkId) {
273
      /** @type {!Array<number>} */
274
      let smplLoopsBytes = this.getSmplLoopsBytes_();
275
      bytes = bytes.concat(
276
        packString(this.smpl.chunkId),
277
        pack(smplLoopsBytes.length + 36, this.uInt32_),
278
        pack(this.smpl.dwManufacturer, this.uInt32_),
279
        pack(this.smpl.dwProduct, this.uInt32_),
280
        pack(this.smpl.dwSamplePeriod, this.uInt32_),
281
        pack(this.smpl.dwMIDIUnityNote, this.uInt32_),
282
        pack(this.smpl.dwMIDIPitchFraction, this.uInt32_),
283
        pack(this.smpl.dwSMPTEFormat, this.uInt32_),
284
        pack(this.smpl.dwSMPTEOffset, this.uInt32_),
285
        pack(this.smpl.dwNumSampleLoops, this.uInt32_),
286
        pack(this.smpl.dwSamplerData, this.uInt32_),
287
        smplLoopsBytes);
288
    }
289
    return bytes;
290
  }
291
292
  /**
293
   * Return the bytes of the 'smpl' loops.
294
   * @return {!Array<number>} The 'smpl' loops as an array of bytes.
295
   * @private
296
   */
297
  getSmplLoopsBytes_() {
298
    /** @type {!Array<number>} */
299
    let loops = [];
300
    for (let i=0; i<this.smpl.dwNumSampleLoops; i++) {
301
      loops = loops.concat(
302
        pack(this.smpl.loops[i].dwName, this.uInt32_),
303
        pack(this.smpl.loops[i].dwType, this.uInt32_),
304
        pack(this.smpl.loops[i].dwStart, this.uInt32_),
305
        pack(this.smpl.loops[i].dwEnd, this.uInt32_),
306
        pack(this.smpl.loops[i].dwFraction, this.uInt32_),
307
        pack(this.smpl.loops[i].dwPlayCount, this.uInt32_));
308
    }
309
    return loops;
310
  }
311
312
  /**
313
   * Return the bytes of the 'fact' chunk.
314
   * @return {!Array<number>} The 'fact' chunk bytes.
315
   * @private
316
   */
317
  getFactBytes_() {
318
    /** @type {!Array<number>} */
319
    let bytes = [];
320
    if (this.fact.chunkId) {
321
      bytes = bytes.concat(
322
        packString(this.fact.chunkId),
323
        pack(this.fact.chunkSize, this.uInt32_),
324
        pack(this.fact.dwSampleLength, this.uInt32_));
325
    }
326
    return bytes;
327
  }
328
329
  /**
330
   * Return the bytes of the 'fmt ' chunk.
331
   * @return {!Array<number>} The 'fmt' chunk bytes.
332
   * @throws {Error} if no 'fmt ' chunk is present.
333
   * @private
334
   */
335
  getFmtBytes_() {
336
    /** @type {!Array<number>} */
337
    let fmtBytes = [];
338
    if (this.fmt.chunkId) {
339
      return fmtBytes.concat(
340
        packString(this.fmt.chunkId),
341
        pack(this.fmt.chunkSize, this.uInt32_),
342
        pack(this.fmt.audioFormat, this.uInt16_),
343
        pack(this.fmt.numChannels, this.uInt16_),
344
        pack(this.fmt.sampleRate, this.uInt32_),
345
        pack(this.fmt.byteRate, this.uInt32_),
346
        pack(this.fmt.blockAlign, this.uInt16_),
347
        pack(this.fmt.bitsPerSample, this.uInt16_),
348
        this.getFmtExtensionBytes_());
349
    }
350
    throw Error('Could not find the "fmt " chunk');
351
  }
352
353
  /**
354
   * Return the bytes of the fmt extension fields.
355
   * @return {!Array<number>} The fmt extension bytes.
356
   * @private
357
   */
358
  getFmtExtensionBytes_() {
359
    /** @type {!Array<number>} */
360
    let extension = [];
361
    if (this.fmt.chunkSize > 16) {
362
      extension = extension.concat(
363
        pack(this.fmt.cbSize, this.uInt16_));
364
    }
365
    if (this.fmt.chunkSize > 18) {
366
      extension = extension.concat(
367
        pack(this.fmt.validBitsPerSample, this.uInt16_));
368
    }
369
    if (this.fmt.chunkSize > 20) {
370
      extension = extension.concat(
371
        pack(this.fmt.dwChannelMask, this.uInt32_));
372
    }
373
    if (this.fmt.chunkSize > 24) {
374
      extension = extension.concat(
375
        pack(this.fmt.subformat[0], this.uInt32_),
376
        pack(this.fmt.subformat[1], this.uInt32_),
377
        pack(this.fmt.subformat[2], this.uInt32_),
378
        pack(this.fmt.subformat[3], this.uInt32_));
379
    }
380
    return extension;
381
  }
382
383
  /**
384
   * Return the bytes of the 'LIST' chunk.
385
   * @return {!Array<number>} The 'LIST' chunk bytes.
386
   * @private
387
   */
388
  getLISTBytes_() {
389
    /** @type {!Array<number>} */
390
    let bytes = [];
391
    for (let i=0; i<this.LIST.length; i++) {
392
      /** @type {!Array<number>} */
393
      let subChunksBytes = this.getLISTSubChunksBytes_(
394
          this.LIST[i].subChunks, this.LIST[i].format);
395
      bytes = bytes.concat(
396
        packString(this.LIST[i].chunkId),
397
        pack(subChunksBytes.length + 4, this.uInt32_),
398
        packString(this.LIST[i].format),
399
        subChunksBytes);
400
    }
401
    return bytes;
402
  }
403
404
  /**
405
   * Return the bytes of the sub chunks of a 'LIST' chunk.
406
   * @param {!Array<!Object>} subChunks The 'LIST' sub chunks.
407
   * @param {string} format The format of the 'LIST' chunk.
408
   *    Currently supported values are 'adtl' or 'INFO'.
409
   * @return {!Array<number>} The sub chunk bytes.
410
   * @private
411
   */
412
  getLISTSubChunksBytes_(subChunks, format) {
413
    /** @type {!Array<number>} */
414
    let bytes = [];
415
    for (let i=0; i<subChunks.length; i++) {
416
      if (format == 'INFO') {
417
        bytes = bytes.concat(
418
          packString(subChunks[i].chunkId),
419
          pack(subChunks[i].value.length + 1, this.uInt32_),
420
          writeString(
421
            subChunks[i].value, subChunks[i].value.length));
422
        bytes.push(0);
423
      } else if (format == 'adtl') {
424
        if (['labl', 'note'].indexOf(subChunks[i].chunkId) > -1) {
425
          bytes = bytes.concat(
426
            packString(subChunks[i].chunkId),
427
            pack(
428
              subChunks[i].value.length + 4 + 1, this.uInt32_),
429
            pack(subChunks[i].dwName, this.uInt32_),
430
            writeString(
431
              subChunks[i].value,
432
              subChunks[i].value.length));
433
          bytes.push(0);
434
        } else if (subChunks[i].chunkId == 'ltxt') {
435
          bytes = bytes.concat(
436
            this.getLtxtChunkBytes_(subChunks[i]));
437
        }
438
      }
439
      if (bytes.length % 2) {
440
        bytes.push(0);
441
      }
442
    }
443
    return bytes;
444
  }
445
446
  /**
447
   * Return the bytes of a 'ltxt' chunk.
448
   * @param {!Object} ltxt the 'ltxt' chunk.
449
   * @private
450
   */
451
  getLtxtChunkBytes_(ltxt) {
452
    return [].concat(
453
      packString(ltxt.chunkId),
454
      pack(ltxt.value.length + 20, this.uInt32_),
455
      pack(ltxt.dwName, this.uInt32_),
456
      pack(ltxt.dwSampleLength, this.uInt32_),
457
      pack(ltxt.dwPurposeID, this.uInt32_),
458
      pack(ltxt.dwCountry, this.uInt16_),
459
      pack(ltxt.dwLanguage, this.uInt16_),
460
      pack(ltxt.dwDialect, this.uInt16_),
461
      pack(ltxt.dwCodePage, this.uInt16_),
462
      writeString(ltxt.value, ltxt.value.length));
463
  }
464
465
  /**
466
   * Return the bytes of the 'junk' chunk.
467
   * @private
468
   */
469
  getJunkBytes_() {
470
    /** @type {!Array<number>} */
471
    let bytes = [];
472
    if (this.junk.chunkId) {
473
      return bytes.concat(
474
        packString(this.junk.chunkId),
475
        pack(this.junk.chunkData.length, this.uInt32_),
476
        this.junk.chunkData);
477
    }
478
    return bytes;
479
  }
480
481
  /**
482
   * Validate the bit depth.
483
   * @return {boolean} True is the bit depth is valid.
484
   * @throws {Error} If bit depth is invalid.
485
   * @private
486
   */
487
  validateBitDepth_() {
488
    if (!this.WAV_AUDIO_FORMATS[this.bitDepth]) {
489
      if (parseInt(this.bitDepth, 10) > 8 &&
490
          parseInt(this.bitDepth, 10) < 54) {
491
        return true;
492
      }
493
      throw new Error('Invalid bit depth.');
494
    }
495
    return true;
496
  }
497
}
498